elastic / kibana

Your window into the Elastic Stack
https://www.elastic.co/products/kibana
Other
19.58k stars 8.21k forks source link

[Security Solution] Support circular discriminated union codegen #186513

Open maximpn opened 4 months ago

maximpn commented 4 months ago

Epic: https://github.com/elastic/security-team/issues/9723 (internal) Relates to: https://github.com/elastic/kibana/issues/186066

Summary

https://github.com/elastic/kibana/pull/186221 added support for local circular references code generation. Zod's discriminated union works only with object schemas which means ZodObject` type.

We need to tune code generation to support circular discriminated unions as well.

Details

Let's consider the following OpenAPI spec where DiscriminatedUnionSchema is a circular discriminated union schema

openapi: 3.0.0
info:
  title: Common Exception List Item Entry Attributes
  version: 'not applicable'
paths: {}
components:
  x-codegen-enabled: true
  schemas:
    SchemaA:
      type: object
      properties:
        type:
          type: string
          enum: ['my-type1']
        field:
          $ref: '#/components/schemas/DiscriminatedUnionSchema'

    SchemaB:
      type: object
      properties:
        type:
          type: string
          enum: ['my-type2']

    DiscriminatedUnionSchema:
      discriminator:
        propertyName: type
      oneOf:
        - $ref: '#/components/schemas/SchemaA'
        - $ref: '#/components/schemas/SchemaB'

Code generation produces the following code

import type { ZodTypeDef } from 'zod';
import { z } from 'zod';

export interface SchemaA {
  type?: 'my-type1';
  field?: DiscriminatedUnionSchema;
}
export interface SchemaAInput {
  type?: 'my-type1';
  field?: DiscriminatedUnionSchemaInput;
}
export const SchemaA: z.ZodType<SchemaA, ZodTypeDef, SchemaAInput> = z.object({
  type: z.literal('my-type1').optional(),
  field: z.lazy(() => DiscriminatedUnionSchema).optional(),
});

export type SchemaB = z.infer<typeof SchemaB>;
export const SchemaB = z.object({
  type: z.literal('my-type2').optional(),
});

export type DiscriminatedUnionSchema = SchemaA | SchemaB;
export type DiscriminatedUnionSchemaInput = SchemaAInput | SchemaB;
export const DiscriminatedUnionSchema: z.ZodType<
  DiscriminatedUnionSchema,
  ZodTypeDef,
  DiscriminatedUnionSchemaInput
> = z.discriminatedUnion('type', [z.lazy(() => SchemaA), SchemaB]);

but there is a problem

image

It's clear that discriminated union doesn't support z.lazy() but removing it doesn't fix the problem

image

To have it fixed SchemaA should be typed as ZodObject. It can achieved either by typing it directly with ZodObject or omit ZodType for it. It's enough to have type hinting for only one zod schemas in a cycle to have types properly inferred. The valid TS code will look like

import type { ZodTypeDef } from 'zod';
import { z } from 'zod';

export interface SchemaA {
  type?: 'my-type1';
  field?: DiscriminatedUnionSchema;
}
export interface SchemaAInput {
  type?: 'my-type1';
  field?: DiscriminatedUnionSchemaInput;
}
export const SchemaA = z.object({
  type: z.literal('my-type1').optional(),
  field: z.lazy(() => DiscriminatedUnionSchema).optional(),
});

export type SchemaB = z.infer<typeof SchemaB>;
export const SchemaB = z.object({
  type: z.literal('my-type2').optional(),
});

export type DiscriminatedUnionSchema = SchemaA | SchemaB;
export type DiscriminatedUnionSchemaInput = SchemaAInput | SchemaB;
export const DiscriminatedUnionSchema: z.ZodType<
  DiscriminatedUnionSchema,
  ZodTypeDef,
  DiscriminatedUnionSchemaInput
> = z.discriminatedUnion('type', [SchemaA, SchemaB]);
elasticmachine commented 4 months ago

Pinging @elastic/security-detections-response (Team:Detections and Resp)

elasticmachine commented 4 months ago

Pinging @elastic/security-solution (Team: SecuritySolution)

elasticmachine commented 4 months ago

Pinging @elastic/security-detection-rule-management (Team:Detection Rule Management)