Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 1 addition & 15 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
"chalk": "^4.1.2",
"chokidar": "^3.5.2",
"fp-ts": "^2.11.5",
"json-schema-faker": "0.5.9",
"jsonrepair": "^3.12.0",
"lodash": "^4.18.1",
"node-fetch": "^2.6.5",
Expand Down
86 changes: 86 additions & 0 deletions packages/cli/src/__tests__/extensions.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { IHttpOperation } from '@stoplight/types';
import { JSONSchema } from '@stoplight/prism-http/src/types';
import { assertRight } from '@stoplight/prism-core/src/__tests__/utils';
import { generate, resetGenerator } from '@stoplight/prism-http/src/mocker/generator/JSONSchema';
import { configureExtensionsUserProvided } from '../extensions';

describe('configureExtensionsUserProvided()', () => {
const operation = {} as IHttpOperation;
// A single optional property is the shape json-schema-faker pads with random extra properties.
const schema: JSONSchema = { type: 'object', properties: { status: { type: 'string' } } };
const spec = (extensions: Record<string, unknown>) => ({
openapi: '3.0.0',
info: { title: 'test', version: '1.0.0' },
paths: {},
...extensions,
});

afterEach(() => resetGenerator());

function expectOnlyDeclaredProperties() {
for (let i = 0; i < 25; i++) {
assertRight(generate(operation, {}, schema), instance => {
expect(Object.keys(instance as object)).toEqual(['status']);
});
}
}

describe('useDefaultValue coupling to fillProperties', () => {
const schemaWithDefault: JSONSchema = {
type: 'object',
required: ['name'],
properties: { name: { type: 'string', default: 'from-default' } },
};

const generatedName = () => {
let name: unknown;
assertRight(generate(operation, {}, schemaWithDefault), instance => {
name = (instance as { name: unknown }).name;
});
return name;
};

it('uses schema defaults while fillProperties is disabled', async () => {
await configureExtensionsUserProvided(spec({}), { fillProperties: false });

expect(generatedName()).toBe('from-default');
});

it('stops using schema defaults when the CLI re-enables fillProperties', async () => {
await configureExtensionsUserProvided(spec({ 'x-json-schema-faker': { fillProperties: false } }), {
fillProperties: true,
});

expect(generatedName()).not.toBe('from-default');
});

it('keeps an explicit useDefaultValue when fillProperties changes', async () => {
await configureExtensionsUserProvided(
spec({ 'x-json-schema-faker': { useDefaultValue: true, fillProperties: false } }),
{ fillProperties: true }
);

expect(generatedName()).toBe('from-default');
});
});

it('applies x-json-schema-faker options to the generator prism-http uses', async () => {
await configureExtensionsUserProvided(spec({ 'x-json-schema-faker': { fillProperties: false } }), {});

expectOnlyDeclaredProperties();
});

it('applies CLI parameters to the generator prism-http uses', async () => {
await configureExtensionsUserProvided(spec({}), { fillProperties: false });

expectOnlyDeclaredProperties();
});

it('lets CLI parameters take precedence over x-json-schema-faker', async () => {
await configureExtensionsUserProvided(spec({ 'x-json-schema-faker': { fillProperties: true } }), {
fillProperties: false,
});

expectOnlyDeclaredProperties();
});
});
26 changes: 4 additions & 22 deletions packages/cli/src/extensions.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import * as $RefParser from '@stoplight/json-schema-ref-parser';
import { decycle } from '@stoplight/json';
import { get, camelCase, forOwn } from 'lodash';
import { JSONSchemaFaker } from 'json-schema-faker';
import type { JSONSchemaFakerOptions } from 'json-schema-faker';
import { resetJSONSchemaGenerator } from '@stoplight/prism-http';
import { get, forOwn } from 'lodash';
import { resetJSONSchemaGenerator, setJSONSchemaGeneratorOption } from '@stoplight/prism-http';

export async function configureExtensionsUserProvided(
specFilePathOrObject: string | object,
Expand All @@ -14,29 +12,13 @@ export async function configureExtensionsUserProvided(
resetJSONSchemaGenerator();

forOwn(get(result, 'x-json-schema-faker', {}), (value: any, option: string) => {
setFakerValue(option, value);
setJSONSchemaGeneratorOption(option, value);
});

// cli parameter takes precidence, so it is set after spec extensions are configed
for (const param in cliParamOptions) {
if (cliParamOptions[param] !== undefined) {
setFakerValue(param, cliParamOptions[param]);
setJSONSchemaGeneratorOption(param, cliParamOptions[param]);
}
}
}

function setFakerValue(option: string, value: any) {
if (option === 'locale') {
// necessary as workaround broken types in json-schema-faker
// @ts-ignore
return JSONSchemaFaker.locate('faker').setLocale(value);
}
// necessary as workaround broken types in json-schema-faker
// @ts-ignore
JSONSchemaFaker.option(camelCase(option) as keyof JSONSchemaFakerOptions, value);
if (camelCase(option) === 'fillProperties' && value === false) {
// When fillProperties is disabled, use schema default values instead of random generation
// @ts-ignore
JSONSchemaFaker.option('useDefaultValue', true);
}
}
2 changes: 1 addition & 1 deletion packages/http/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export * from './mocker/errors';
export * from './router/errors';
export * from './mocker/serializer/style';
export { generate as generateHttpParam } from './mocker/generator/HttpParamGenerator';
export { resetJSONSchemaGenerator } from './mocker';
export { resetJSONSchemaGenerator, setJSONSchemaGeneratorOption } from './mocker';
import { IHttpConfig, IHttpResponse, IHttpRequest, PickRequired, PrismHttpComponents, IHttpProxyConfig } from './types';
export { getHttpOperationsFromSpec } from './utils/operations';
export { createAndCallPrismInstanceWithSpec, PrismErrorResult, PrismOkResult } from './instanceWithSpec';
Expand Down
28 changes: 27 additions & 1 deletion packages/http/src/mocker/generator/JSONSchema.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { faker } from '@faker-js/faker';
import { cloneDeep } from 'lodash';
import { camelCase, cloneDeep } from 'lodash';
import { JSONSchema } from '../../types';

import { JSONSchemaFaker } from 'json-schema-faker';
Expand Down Expand Up @@ -46,6 +46,31 @@ const JSON_SCHEMA_FAKER_DEFAULT_OPTIONS = Object.fromEntries([
['omitNulls', false],
]);

let useDefaultValueSetExplicitly = false;

// Options must be set through this module: a second json-schema-faker copy installed for another
// package (e.g. a version skew with prism-cli) is a separate instance the generator never reads.
export function setGeneratorOption(option: string, value: unknown) {
const name = camelCase(option);
if (name === 'locale') {
// necessary as workaround broken types in json-schema-faker
// @ts-ignore
return JSONSchemaFaker.locate('faker').setLocale(value);
}
if (name === 'useDefaultValue') {
useDefaultValueSetExplicitly = true;
}
// necessary as workaround broken types in json-schema-faker
// @ts-ignore
JSONSchemaFaker.option(name, value);
// Without fillProperties nothing is invented for missing values, so fall back to schema defaults
// unless the user configured useDefaultValue themselves.
if (name === 'fillProperties' && !useDefaultValueSetExplicitly) {
// @ts-ignore
JSONSchemaFaker.option('useDefaultValue', value === false);
}
}

export function resetGenerator() {
// necessary as workaround broken types in json-schema-faker
// @ts-ignore
Expand All @@ -58,6 +83,7 @@ export function resetGenerator() {
fixedProbabilities: true,
ignoreMissingRefs: true,
});
useDefaultValueSetExplicitly = false;
}

resetGenerator();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { get } from 'lodash';
import { JSONSchema } from '../../../types';
import { generate, sortSchemaAlphabetically } from '../JSONSchema';
import { generate, resetGenerator, setGeneratorOption, sortSchemaAlphabetically } from '../JSONSchema';
import { assertRight, assertLeft } from '@stoplight/prism-core/src/__tests__/utils';
import { IHttpOperation } from '@stoplight/types';

Expand Down Expand Up @@ -199,6 +199,26 @@ describe('JSONSchema generator', () => {
});
});

describe('when fillProperties is disabled', () => {
const schema: JSONSchema = {
type: 'object',
properties: {
status: { type: 'string' },
},
};

beforeEach(() => setGeneratorOption('fillProperties', false));
afterEach(() => resetGenerator());

it('will not add properties that are not declared in the schema', () => {
for (let i = 0; i < 25; i++) {
assertRight(generate(operation, {}, schema), instance => {
expect(Object.keys(instance as object)).toEqual(['status']);
});
}
});
});

it('operates on sealed schema objects', () => {
const schema: JSONSchema = {
type: 'object',
Expand Down
5 changes: 4 additions & 1 deletion packages/http/src/mocker/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,10 @@ import {
} from '../validator/validators/body';
import { parseMIMEHeader } from '../validator/validators/headers';
import { NonEmptyArray } from 'fp-ts/NonEmptyArray';
export { resetGenerator as resetJSONSchemaGenerator } from './generator/JSONSchema';
export {
resetGenerator as resetJSONSchemaGenerator,
setGeneratorOption as setJSONSchemaGeneratorOption,
} from './generator/JSONSchema';

const eitherRecordSequence = Record.sequence(E.Applicative);
const eitherSequence = sequenceT(E.Apply);
Expand Down