-
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.
[Security Solution] Generate artefacts (Zod schemas + TS types) in a …
…proper order (#187044) **Closes:** #182928 ## Summary This PR modifies `kbn-openapi-generator` to produce Zod schemas and related TS types for shared schemas in `components.schemas` in a proper order independent from OpenAPI spec schemas definition order. ## Details Current `kbn-openapi-generator` implementation to generate Zod schemas and related TS types for OpenAPI's spec file `components.schemas` is straightforward. It repeats schemas definition order which can lead to cases when a dependent artefact is defined before its dependencies. Engineers have to manually order schemas in OpenAPI spec files to make sure produce artefacts are valid TS files while OpenAPI specification doesn't impose such requirements. To illustrate the problem let's consider a following OpenAPI spec ```yaml ... components: x-codegen-enabled: true schemas: MainSchema: type: object properties: fieldA: $ref: '#/components/schemas/DepA' nullable: true fieldB: $ref: '#/components/schemas/DepB' fieldC: $ref: '#/components/schemas/DepC' DepB: type: boolean DepC: type: integer DepA: type: string ``` Running code generation for the spec above produces the following artefacts ```ts import { z } from 'zod'; export type MainSchema = z.infer<typeof MainSchema>; export const MainSchema = z.object({ fieldA: DepA.nullable().optional(), fieldB: DepB.optional(), fieldC: DepC.optional(), }); export type DepB = z.infer<typeof DepB>; export const DepB = z.boolean(); export type DepC = z.infer<typeof DepC>; export const DepC = z.number().int(); export type DepA = z.infer<typeof DepA>; export const DepA = z.string(); ``` which is not valid since dependencies are defined after the dependent `MainSchema`. ### After the fix The fix takes into account that references chain represents a graph in a common case. When there are no cycles in references they represent a [Directed Acyclic Graph (DAG)](https://en.wikipedia.org/wiki/Directed_acyclic_graph). In graph's terminology schema is a node and reference is an edge. There are `[topological sorting algorithms](https://en.wikipedia.org/wiki/Topological_sorting)` which order DAG nodes starting from zero incoming edges to the maximum number. There is an ability to take into account cycles so the result has sorted nodes which don't take part in cycles. References sorted this way have dependencies defined before dependent schemas. After applying this fix and running code generation for the OpenAPI spec above we will get the following ```ts import { z } from 'zod'; export type DepA = z.infer<typeof DepA>; export const DepA = z.string(); export type DepB = z.infer<typeof DepB>; export const DepB = z.boolean(); export type DepC = z.infer<typeof DepC>; export const DepC = z.number().int(); export type MainSchema = z.infer<typeof MainSchema>; export const MainSchema = z.object({ fieldA: DepA.nullable().optional(), fieldB: DepB.optional(), fieldC: DepC.optional(), }); ``` ### Notes - Implementation preserves original schemas order when possible. Generally speaking topological sorting doesn't define relative order in a group of elements with the same number of incoming edges (dependencies in our case). It means the result ordering can vary. **To reduce the diff topological sorting implementation in this PR preserves original schemas order when possible**. When sorting is necessary schemas are places in dependencies discovery order. You can see that OpenAPI spec above has schemas ordered like `DepB`, `DepC` and `DepA` while prodiced TS file has them ordered `DepA`, `DepB` and `DepC` since it's the order the dependencies were discovered in the `MainSchema`. - There are two way in how to implement topological sorting Depth-First Search (DFS) and Breadth-First Search (BFS). Implementation in this PR uses recursive DFS implementation since it allows to preserve the original order where possible and has better readability.
- Loading branch information
Showing
9 changed files
with
206 additions
and
33 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
17 changes: 17 additions & 0 deletions
17
packages/kbn-openapi-generator/src/parser/lib/helpers/find_local_refs.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,17 @@ | ||
/* | ||
* 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 { findRefs } from './find_refs'; | ||
import { isLocalRef } from './is_local_ref'; | ||
|
||
/** | ||
* Finds local references | ||
*/ | ||
export function findLocalRefs(obj: unknown): string[] { | ||
return findRefs(obj).filter((ref) => isLocalRef(ref)); | ||
} |
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
15 changes: 15 additions & 0 deletions
15
packages/kbn-openapi-generator/src/parser/lib/helpers/is_local_ref.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,15 @@ | ||
/* | ||
* 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. | ||
*/ | ||
|
||
/** | ||
* Checks whether the provided ref is local. | ||
* Local references start with `#/` | ||
*/ | ||
export function isLocalRef(ref: string): boolean { | ||
return ref.startsWith('#/'); | ||
} |
46 changes: 46 additions & 0 deletions
46
packages/kbn-openapi-generator/src/parser/lib/helpers/parse_ref.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,46 @@ | ||
/* | ||
* 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. | ||
*/ | ||
|
||
interface ParsedRef { | ||
uri: string; | ||
pointer: string; | ||
schemaName: string; | ||
} | ||
|
||
/** | ||
* Parses an OpenAPI reference a.k.a JSON reference | ||
* See https://datatracker.ietf.org/doc/html/draft-pbryan-zyp-json-ref-03 | ||
* | ||
* JSON reference consists of an optional uri and required JSON pointer | ||
* looking like `uri#pointer`. While RFC implies URI usage mostly relative | ||
* paths are used. | ||
* | ||
* An example looks like | ||
* | ||
* ``` | ||
* ../path/to/my/file.schema.yaml#/components/schemas/MySchema | ||
* ``` | ||
* | ||
* This function returns `uri`, JSON `pointer` and | ||
* `schemaName` which is the last part of the JSON pointer. In the example | ||
* above `schemaName` is `MySchema`. | ||
*/ | ||
export function parseRef(ref: string): ParsedRef { | ||
if (!ref.includes('#')) { | ||
throw new Error(`Reference parse error: provided ref is not valid "${ref}"`); | ||
} | ||
|
||
const [uri, pointer] = ref.split('#'); | ||
const schemaName = pointer.split('/').at(-1)!; | ||
|
||
return { | ||
uri, | ||
pointer, | ||
schemaName, | ||
}; | ||
} |
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