-
Notifications
You must be signed in to change notification settings - Fork 8.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[HTTP/OAS] Lazy response schemas (#181622)
## Summary Based on the introduction of new response schemas for OAS generation we are going to start the long tail of introducing missing response (`joi`) schemas. We have roughly 520 known public APIs, most of which do not have response schemas defined. We expected a fairly large increase in `@kbn/config-schema` definitions in the coming weeks/months. Regardless of actual outcome and given how slow schema instantiation is, this presents a slight concern for startup time. ## Proposed changes Give consumers guidance and a way to pass in validation lazily. Under the hood we make sure that the lazy schemas only get called once. ```ts /** * A validation schema factory. * * @note Used to lazily create schemas that are otherwise not needed * @note Assume this function will only be called once * * @return A @kbn/config-schema schema * @public */ export type LazyValidator = () => Type<unknown>; /** @public */ export interface VersionedRouteCustomResponseBodyValidation { /** A custom validation function */ custom: RouteValidationFunction<unknown>; } /** @public */ export type VersionedResponseBodyValidation = | LazyValidator | VersionedRouteCustomResponseBodyValidation; /** * Map of response status codes to response schemas * * @note Instantiating response schemas is expensive, especially when it is * not needed in most cases. See example below to ensure this is lazily * provided. * * @note The {@link TypeOf} type utility from @kbn/config-schema can extract * types from lazily created schemas * * @example * ```ts * // Avoid this: * const badResponseSchema = schema.object({ foo: foo.string() }); * // Do this: * const goodResponseSchema = () => schema.object({ foo: foo.string() }); * * type ResponseType = TypeOf<typeof goodResponseSchema>; * ... * .addVersion( * { ... validation: { response: { 200: { body: goodResponseSchema } } } }, * handlerFn * ) * ... * ``` * @public */ export interface VersionedRouteResponseValidation { [statusCode: number]: { body: VersionedResponseBodyValidation; }; unsafe?: { body?: boolean }; } ``` ## Notes * Expected (worst case) in low resource environments is an additional 1.5 seconds to start up time and additional ~70MB to memory pressure which is not a great trade-off for functionality that is only used when OAS generation is on. Related #181277
- Loading branch information
1 parent
c96a560
commit 97e1d9f
Showing
56 changed files
with
713 additions
and
248 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
44 changes: 44 additions & 0 deletions
44
packages/core/http/core-http-router-server-internal/src/util.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License | ||
* 2.0 and the Server Side Public License, v 1; you may not use this file except | ||
* in compliance with, at your election, the Elastic License 2.0 or the Server | ||
* Side Public License, v 1. | ||
*/ | ||
|
||
import { schema } from '@kbn/config-schema'; | ||
import { RouteValidator } from '@kbn/core-http-server'; | ||
import { prepareResponseValidation } from './util'; | ||
|
||
describe('prepareResponseValidation', () => { | ||
it('wraps only expected values in "once"', () => { | ||
const validation: RouteValidator<unknown, unknown, unknown> = { | ||
request: {}, | ||
response: { | ||
200: { | ||
body: jest.fn(() => schema.string()), | ||
}, | ||
404: { | ||
body: jest.fn(() => schema.string()), | ||
}, | ||
unsafe: { | ||
body: true, | ||
}, | ||
}, | ||
}; | ||
|
||
const prepared = prepareResponseValidation(validation.response!); | ||
|
||
expect(prepared).toEqual({ | ||
200: { body: expect.any(Function) }, | ||
404: { body: expect.any(Function) }, | ||
unsafe: { body: true }, | ||
}); | ||
|
||
[1, 2, 3].forEach(() => prepared[200].body()); | ||
[1, 2, 3].forEach(() => prepared[404].body()); | ||
|
||
expect(validation.response![200].body).toHaveBeenCalledTimes(1); | ||
expect(validation.response![404].body).toHaveBeenCalledTimes(1); | ||
}); | ||
}); |
66 changes: 66 additions & 0 deletions
66
packages/core/http/core-http-router-server-internal/src/util.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License | ||
* 2.0 and the Server Side Public License, v 1; you may not use this file except | ||
* in compliance with, at your election, the Elastic License 2.0 or the Server | ||
* Side Public License, v 1. | ||
*/ | ||
|
||
import { once } from 'lodash'; | ||
import { | ||
isFullValidatorContainer, | ||
type RouteConfig, | ||
type RouteMethod, | ||
type RouteValidator, | ||
} from '@kbn/core-http-server'; | ||
import type { ObjectType, Type } from '@kbn/config-schema'; | ||
|
||
function isStatusCode(key: string) { | ||
return !isNaN(parseInt(key, 10)); | ||
} | ||
|
||
interface ResponseValidation { | ||
[statusCode: number]: { body: () => ObjectType | Type<unknown> }; | ||
} | ||
|
||
export function prepareResponseValidation(validation: ResponseValidation): ResponseValidation { | ||
const responses = Object.entries(validation).map(([key, value]) => { | ||
if (isStatusCode(key)) { | ||
return [key, { body: once(value.body) }]; | ||
} | ||
return [key, value]; | ||
}); | ||
|
||
return Object.fromEntries(responses); | ||
} | ||
|
||
function prepareValidation<P, Q, B>(validator: RouteValidator<P, Q, B>) { | ||
if (isFullValidatorContainer(validator) && validator.response) { | ||
return { | ||
...validator, | ||
response: prepareResponseValidation(validator.response), | ||
}; | ||
} | ||
return validator; | ||
} | ||
|
||
// Integration tested in ./routes.test.ts | ||
export function prepareRouteConfigValidation<P, Q, B>( | ||
config: RouteConfig<P, Q, B, RouteMethod> | ||
): RouteConfig<P, Q, B, RouteMethod> { | ||
// Calculating schema validation can be expensive so when it is provided lazily | ||
// we only want to instantiate it once. This also provides idempotency guarantees | ||
if (typeof config.validate === 'function') { | ||
const validate = config.validate; | ||
return { | ||
...config, | ||
validate: once(() => prepareValidation(validate())), | ||
}; | ||
} else if (typeof config.validate === 'object' && typeof config.validate !== null) { | ||
return { | ||
...config, | ||
validate: prepareValidation(config.validate), | ||
}; | ||
} | ||
return config; | ||
} |
Oops, something went wrong.