diff --git a/package-lock.json b/package-lock.json index 2e95af5a1..9f5dfed99 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "prism", + "name": "prism-fix", "version": "0.0.0", "lockfileVersion": 3, "requires": true, @@ -14772,7 +14772,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", @@ -14805,19 +14804,6 @@ "wrap-ansi": "^7.0.0" } }, - "packages/cli/node_modules/json-schema-faker": { - "version": "0.5.9", - "resolved": "https://registry.npmjs.org/json-schema-faker/-/json-schema-faker-0.5.9.tgz", - "integrity": "sha512-fNKLHgDvfGNNTX1zqIjqFMJjCLzJ2kvnJ831x4aqkAoeE4jE2TxvpJdhOnk3JU3s42vFzmXvkpbYzH5H3ncAzg==", - "license": "MIT", - "dependencies": { - "json-schema-ref-parser": "^6.1.0", - "jsonpath-plus": "^10.3.0" - }, - "bin": { - "jsf": "bin/gen.cjs" - } - }, "packages/cli/node_modules/yargs": { "version": "16.2.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.2.tgz", diff --git a/packages/cli/package.json b/packages/cli/package.json index 7cb1a27c4..65f9be42b 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -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", diff --git a/packages/cli/src/__tests__/extensions.spec.ts b/packages/cli/src/__tests__/extensions.spec.ts new file mode 100644 index 000000000..b8a71a06b --- /dev/null +++ b/packages/cli/src/__tests__/extensions.spec.ts @@ -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) => ({ + 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(); + }); +}); diff --git a/packages/cli/src/extensions.ts b/packages/cli/src/extensions.ts index c60bc96fd..b3b8aafa7 100644 --- a/packages/cli/src/extensions.ts +++ b/packages/cli/src/extensions.ts @@ -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, @@ -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); - } -} diff --git a/packages/http/src/index.ts b/packages/http/src/index.ts index f151c559a..079593c26 100644 --- a/packages/http/src/index.ts +++ b/packages/http/src/index.ts @@ -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'; diff --git a/packages/http/src/mocker/generator/JSONSchema.ts b/packages/http/src/mocker/generator/JSONSchema.ts index dc0071a12..e53806e0e 100644 --- a/packages/http/src/mocker/generator/JSONSchema.ts +++ b/packages/http/src/mocker/generator/JSONSchema.ts @@ -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'; @@ -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 @@ -58,6 +83,7 @@ export function resetGenerator() { fixedProbabilities: true, ignoreMissingRefs: true, }); + useDefaultValueSetExplicitly = false; } resetGenerator(); diff --git a/packages/http/src/mocker/generator/__tests__/JSONSchema.spec.ts b/packages/http/src/mocker/generator/__tests__/JSONSchema.spec.ts index cc6fc9240..2e7a33a71 100644 --- a/packages/http/src/mocker/generator/__tests__/JSONSchema.spec.ts +++ b/packages/http/src/mocker/generator/__tests__/JSONSchema.spec.ts @@ -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'; @@ -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', diff --git a/packages/http/src/mocker/index.ts b/packages/http/src/mocker/index.ts index b566fa063..1e0507aaa 100644 --- a/packages/http/src/mocker/index.ts +++ b/packages/http/src/mocker/index.ts @@ -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);