fork of hey-api/openapi-ts because I need some additional things

Changes before error encountered

Co-authored-by: mrlubos <12529395+mrlubos@users.noreply.github.com>

+272 -92
+6 -12
packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/discriminator-all-of/types.gen.ts
··· 9 9 }; 10 10 11 11 export type Bar = Foo & { 12 + bar?: string; 12 13 id: 'Bar'; 13 - } & { 14 - bar?: string; 15 14 }; 16 15 17 16 export type Baz = Foo & { 17 + baz?: string; 18 18 id: 'Baz'; 19 - } & { 20 - baz?: string; 21 19 }; 22 20 23 21 export type Qux = Foo & { 24 - id: 'Qux'; 25 - } & { 26 22 qux?: boolean; 23 + id: 'Qux'; 27 24 }; 28 25 29 26 export type FooMapped = { ··· 31 28 }; 32 29 33 30 export type BarMapped = FooMapped & { 31 + bar?: string; 34 32 id: 'bar'; 35 - } & { 36 - bar?: string; 37 33 }; 38 34 39 35 export type BazMapped = FooMapped & { 36 + baz?: string; 40 37 id: 'baz'; 41 - } & { 42 - baz?: string; 43 38 }; 44 39 45 40 export type QuxMapped = FooMapped & { 46 - id: 'QuxMapped'; 47 - } & { 48 41 qux?: boolean; 42 + id: 'QuxMapped'; 49 43 }; 50 44 51 45 export type FooUnion = ({
+2 -4
packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/discriminator-allof-nested/types.gen.ts
··· 10 10 }; 11 11 12 12 export type CarDto = VehicleDto & { 13 - $type: 'Car'; 14 - } & { 15 13 modelName: string; 14 + $type: 'Car' | 'Volvo'; 16 15 }; 17 16 18 17 export type VolvoDto = CarDto & { 18 + seatbeltCount: number; 19 19 $type: 'Volvo'; 20 - } & { 21 - seatbeltCount: number; 22 20 }; 23 21 24 22 export type GetCarsData = {
+1 -2
packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/transformers-all-of/types.gen.ts
··· 14 14 }; 15 15 16 16 export type Baz = Qux & { 17 - id: 'Baz'; 18 - } & { 19 17 foo: number; 20 18 bar: Date; 21 19 baz: 'foo' | 'bar' | 'baz'; 22 20 qux: number; 21 + id: 'Baz'; 23 22 }; 24 23 25 24 export type Qux = {
+6 -12
packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/discriminator-all-of/types.gen.ts
··· 9 9 }; 10 10 11 11 export type Bar = Foo & { 12 + bar?: string; 12 13 id: 'Bar'; 13 - } & { 14 - bar?: string; 15 14 }; 16 15 17 16 export type Baz = Foo & { 17 + baz?: string; 18 18 id: 'Baz'; 19 - } & { 20 - baz?: string; 21 19 }; 22 20 23 21 export type Qux = Foo & { 24 - id: 'Qux'; 25 - } & { 26 22 qux?: boolean; 23 + id: 'Qux'; 27 24 }; 28 25 29 26 export type FooMapped = { ··· 31 28 }; 32 29 33 30 export type BarMapped = FooMapped & { 31 + bar?: string; 34 32 id: 'bar'; 35 - } & { 36 - bar?: string; 37 33 }; 38 34 39 35 export type BazMapped = FooMapped & { 36 + baz?: string; 40 37 id: 'baz'; 41 - } & { 42 - baz?: string; 43 38 }; 44 39 45 40 export type QuxMapped = FooMapped & { 46 - id: 'QuxMapped'; 47 - } & { 48 41 qux?: boolean; 42 + id: 'QuxMapped'; 49 43 }; 50 44 51 45 export type FooUnion = ({
+2 -4
packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/discriminator-allof-nested/types.gen.ts
··· 10 10 }; 11 11 12 12 export type CarDto = VehicleDto & { 13 - $type: 'Car'; 14 - } & { 15 13 modelName: string; 14 + $type: 'Car' | 'Volvo'; 16 15 }; 17 16 18 17 export type VolvoDto = CarDto & { 18 + seatbeltCount: number; 19 19 $type: 'Volvo'; 20 - } & { 21 - seatbeltCount: number; 22 20 }; 23 21 24 22 export type GetCarsData = {
+1 -2
packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/transformers-all-of/types.gen.ts
··· 14 14 }; 15 15 16 16 export type Baz = Qux & { 17 - id: 'Baz'; 18 - } & { 19 17 foo: number; 20 18 bar: Date; 21 19 baz: 'foo' | 'bar' | 'baz'; 22 20 qux: number; 21 + id: 'Baz'; 23 22 }; 24 23 25 24 export type Qux = {
+127 -28
packages/openapi-ts/src/openApi/3.0.x/parser/schema.ts
··· 79 79 return discriminators; 80 80 }; 81 81 82 + /** 83 + * Gets all discriminator values for a schema and its children in the inheritance hierarchy. 84 + * For intermediate schemas (those that are extended by others), returns a union of all values. 85 + */ 86 + const getAllDiscriminatorValues = ({ 87 + context, 88 + discriminator, 89 + schemaRef, 90 + }: { 91 + context: Context; 92 + discriminator: NonNullable<SchemaObject['discriminator']>; 93 + schemaRef: string; 94 + }): string[] => { 95 + const values: string[] = []; 96 + 97 + // Check each entry in the discriminator mapping 98 + for (const [value, mappedSchemaRef] of Object.entries( 99 + discriminator.mapping || {}, 100 + )) { 101 + if (mappedSchemaRef === schemaRef) { 102 + // This is the current schema's own value 103 + values.push(value); 104 + continue; 105 + } 106 + 107 + // Check if the mapped schema extends the current schema 108 + const mappedSchema = context.resolveRef<SchemaObject>(mappedSchemaRef); 109 + if (mappedSchema.allOf) { 110 + for (const item of mappedSchema.allOf) { 111 + if ('$ref' in item && item.$ref === schemaRef) { 112 + // This schema extends the current schema, add its value 113 + values.push(value); 114 + break; 115 + } 116 + } 117 + } 118 + } 119 + 120 + return values; 121 + }; 122 + 82 123 const parseSchemaJsDoc = ({ 83 124 irSchema, 84 125 schema, ··· 358 399 359 400 const compositionSchemas = schema.allOf; 360 401 402 + // Collect discriminator information to add after all compositions are processed 403 + type DiscriminatorInfo = { 404 + discriminator: NonNullable<SchemaObject['discriminator']>; 405 + values: string[]; 406 + isRequired: boolean; 407 + }; 408 + const discriminatorsToAdd: DiscriminatorInfo[] = []; 409 + const addedDiscriminators = new Set<string>(); 410 + 361 411 for (const compositionSchema of compositionSchemas) { 362 412 const originalInAllOf = state.inAllOf; 363 413 // Don't propagate inAllOf flag to $ref schemas to avoid issues with reusable components ··· 397 447 schema: ref, 398 448 }); 399 449 400 - // Track discriminator properties we've already added to avoid duplicates 401 - const addedDiscriminators = new Set<string>(); 402 - 403 450 // Process each discriminator found 404 451 for (const { discriminator, oneOf } of discriminators) { 405 - // Skip if we've already added this discriminator property 452 + // Skip if we've already collected this discriminator property 406 453 if (addedDiscriminators.has(discriminator.propertyName)) { 407 454 continue; 408 455 } ··· 419 466 ); 420 467 421 468 if (values.length > 0) { 422 - const valueSchemas: ReadonlyArray<IR.SchemaObject> = values.map( 423 - (value) => ({ 424 - const: value, 425 - type: 'string', 426 - }), 427 - ); 428 - const irDiscriminatorSchema: IR.SchemaObject = { 429 - properties: { 430 - [discriminator.propertyName]: 431 - valueSchemas.length > 1 432 - ? { 433 - items: valueSchemas, 434 - logicalOperator: 'or', 435 - } 436 - : valueSchemas[0]!, 437 - }, 438 - type: 'object', 439 - }; 440 - 441 469 // Check if the discriminator property is required in any of the discriminator schemas 442 - // by looking at all discriminators with the same property name 443 470 const isRequired = discriminators.some( 444 471 (d) => 445 472 d.discriminator.propertyName === discriminator.propertyName && ··· 457 484 }))), 458 485 ); 459 486 460 - if (isRequired) { 461 - irDiscriminatorSchema.required = [discriminator.propertyName]; 462 - } 463 - schemaItems.push(irDiscriminatorSchema); 487 + discriminatorsToAdd.push({ 488 + discriminator, 489 + values, 490 + isRequired, 491 + }); 464 492 addedDiscriminators.add(discriminator.propertyName); 465 493 } 466 494 } 467 495 } 496 + } 497 + } 498 + 499 + // Now add discriminators after all compositions have been processed 500 + for (const { discriminator, values, isRequired } of discriminatorsToAdd) { 501 + // Get all discriminator values including children for union types 502 + const allValues = getAllDiscriminatorValues({ 503 + context, 504 + discriminator, 505 + schemaRef: state.$ref!, 506 + }); 507 + 508 + // Use allValues if we found children, otherwise use the original values 509 + const finalValues = allValues.length > 0 ? allValues : values; 510 + 511 + const valueSchemas: ReadonlyArray<IR.SchemaObject> = finalValues.map( 512 + (value) => ({ 513 + const: value, 514 + type: 'string', 515 + }), 516 + ); 517 + 518 + const discriminatorProperty: IR.SchemaObject = 519 + valueSchemas.length > 1 520 + ? { 521 + items: valueSchemas, 522 + logicalOperator: 'or', 523 + } 524 + : valueSchemas[0]!; 525 + 526 + // Find the inline schema (non-$ref) to merge the discriminator property into 527 + // The inline schema should be the last non-$ref item in schemaItems 528 + let inlineSchema: IR.SchemaObject | undefined; 529 + for (let i = schemaItems.length - 1; i >= 0; i--) { 530 + const item = schemaItems[i]; 531 + // Check if this is not a $ref schema by looking for properties or checking if it came from an inline schema 532 + if (item.type === 'object' || item.properties) { 533 + inlineSchema = item; 534 + break; 535 + } 536 + } 537 + 538 + // If we found an inline schema, add the discriminator property to it 539 + if (inlineSchema) { 540 + if (!inlineSchema.properties) { 541 + inlineSchema.properties = {}; 542 + } 543 + inlineSchema.properties[discriminator.propertyName] = 544 + discriminatorProperty; 545 + 546 + if (isRequired) { 547 + if (!inlineSchema.required) { 548 + inlineSchema.required = []; 549 + } 550 + if (!inlineSchema.required.includes(discriminator.propertyName)) { 551 + inlineSchema.required.push(discriminator.propertyName); 552 + } 553 + } 554 + } else { 555 + // Fallback: create a separate discriminator schema if no inline schema found 556 + const irDiscriminatorSchema: IR.SchemaObject = { 557 + properties: { 558 + [discriminator.propertyName]: discriminatorProperty, 559 + }, 560 + type: 'object', 561 + }; 562 + 563 + if (isRequired) { 564 + irDiscriminatorSchema.required = [discriminator.propertyName]; 565 + } 566 + schemaItems.push(irDiscriminatorSchema); 468 567 } 469 568 } 470 569
+127 -28
packages/openapi-ts/src/openApi/3.1.x/parser/schema.ts
··· 83 83 return discriminators; 84 84 }; 85 85 86 + /** 87 + * Gets all discriminator values for a schema and its children in the inheritance hierarchy. 88 + * For intermediate schemas (those that are extended by others), returns a union of all values. 89 + */ 90 + const getAllDiscriminatorValues = ({ 91 + context, 92 + discriminator, 93 + schemaRef, 94 + }: { 95 + context: Context; 96 + discriminator: NonNullable<SchemaObject['discriminator']>; 97 + schemaRef: string; 98 + }): string[] => { 99 + const values: string[] = []; 100 + 101 + // Check each entry in the discriminator mapping 102 + for (const [value, mappedSchemaRef] of Object.entries( 103 + discriminator.mapping || {}, 104 + )) { 105 + if (mappedSchemaRef === schemaRef) { 106 + // This is the current schema's own value 107 + values.push(value); 108 + continue; 109 + } 110 + 111 + // Check if the mapped schema extends the current schema 112 + const mappedSchema = context.resolveRef<SchemaObject>(mappedSchemaRef); 113 + if (mappedSchema.allOf) { 114 + for (const item of mappedSchema.allOf) { 115 + if (item.$ref && item.$ref === schemaRef) { 116 + // This schema extends the current schema, add its value 117 + values.push(value); 118 + break; 119 + } 120 + } 121 + } 122 + } 123 + 124 + return values; 125 + }; 126 + 86 127 const parseSchemaJsDoc = ({ 87 128 irSchema, 88 129 schema, ··· 440 481 441 482 const compositionSchemas = schema.allOf; 442 483 484 + // Collect discriminator information to add after all compositions are processed 485 + type DiscriminatorInfo = { 486 + discriminator: NonNullable<SchemaObject['discriminator']>; 487 + values: string[]; 488 + isRequired: boolean; 489 + }; 490 + const discriminatorsToAdd: DiscriminatorInfo[] = []; 491 + const addedDiscriminators = new Set<string>(); 492 + 443 493 for (const compositionSchema of compositionSchemas) { 444 494 const originalInAllOf = state.inAllOf; 445 495 // Don't propagate inAllOf flag to $ref schemas to avoid issues with reusable components ··· 479 529 schema: ref, 480 530 }); 481 531 482 - // Track discriminator properties we've already added to avoid duplicates 483 - const addedDiscriminators = new Set<string>(); 484 - 485 532 // Process each discriminator found 486 533 for (const { discriminator, oneOf } of discriminators) { 487 - // Skip if we've already added this discriminator property 534 + // Skip if we've already collected this discriminator property 488 535 if (addedDiscriminators.has(discriminator.propertyName)) { 489 536 continue; 490 537 } ··· 501 548 ); 502 549 503 550 if (values.length > 0) { 504 - const valueSchemas: ReadonlyArray<IR.SchemaObject> = values.map( 505 - (value) => ({ 506 - const: value, 507 - type: 'string', 508 - }), 509 - ); 510 - const irDiscriminatorSchema: IR.SchemaObject = { 511 - properties: { 512 - [discriminator.propertyName]: 513 - valueSchemas.length > 1 514 - ? { 515 - items: valueSchemas, 516 - logicalOperator: 'or', 517 - } 518 - : valueSchemas[0]!, 519 - }, 520 - type: 'object', 521 - }; 522 - 523 551 // Check if the discriminator property is required in any of the discriminator schemas 524 - // by looking at all discriminators with the same property name 525 552 const isRequired = discriminators.some( 526 553 (d) => 527 554 d.discriminator.propertyName === discriminator.propertyName && ··· 538 565 }))), 539 566 ); 540 567 541 - if (isRequired) { 542 - irDiscriminatorSchema.required = [discriminator.propertyName]; 543 - } 544 - schemaItems.push(irDiscriminatorSchema); 568 + discriminatorsToAdd.push({ 569 + discriminator, 570 + values, 571 + isRequired, 572 + }); 545 573 addedDiscriminators.add(discriminator.propertyName); 546 574 } 547 575 } 548 576 } 577 + } 578 + } 579 + 580 + // Now add discriminators after all compositions have been processed 581 + for (const { discriminator, values, isRequired } of discriminatorsToAdd) { 582 + // Get all discriminator values including children for union types 583 + const allValues = getAllDiscriminatorValues({ 584 + context, 585 + discriminator, 586 + schemaRef: state.$ref!, 587 + }); 588 + 589 + // Use allValues if we found children, otherwise use the original values 590 + const finalValues = allValues.length > 0 ? allValues : values; 591 + 592 + const valueSchemas: ReadonlyArray<IR.SchemaObject> = finalValues.map( 593 + (value) => ({ 594 + const: value, 595 + type: 'string', 596 + }), 597 + ); 598 + 599 + const discriminatorProperty: IR.SchemaObject = 600 + valueSchemas.length > 1 601 + ? { 602 + items: valueSchemas, 603 + logicalOperator: 'or', 604 + } 605 + : valueSchemas[0]!; 606 + 607 + // Find the inline schema (non-$ref) to merge the discriminator property into 608 + // The inline schema should be the last non-$ref item in schemaItems 609 + let inlineSchema: IR.SchemaObject | undefined; 610 + for (let i = schemaItems.length - 1; i >= 0; i--) { 611 + const item = schemaItems[i]; 612 + // Check if this is not a $ref schema by looking for properties or checking if it came from an inline schema 613 + if (item.type === 'object' || item.properties) { 614 + inlineSchema = item; 615 + break; 616 + } 617 + } 618 + 619 + // If we found an inline schema, add the discriminator property to it 620 + if (inlineSchema) { 621 + if (!inlineSchema.properties) { 622 + inlineSchema.properties = {}; 623 + } 624 + inlineSchema.properties[discriminator.propertyName] = 625 + discriminatorProperty; 626 + 627 + if (isRequired) { 628 + if (!inlineSchema.required) { 629 + inlineSchema.required = []; 630 + } 631 + if (!inlineSchema.required.includes(discriminator.propertyName)) { 632 + inlineSchema.required.push(discriminator.propertyName); 633 + } 634 + } 635 + } else { 636 + // Fallback: create a separate discriminator schema if no inline schema found 637 + const irDiscriminatorSchema: IR.SchemaObject = { 638 + properties: { 639 + [discriminator.propertyName]: discriminatorProperty, 640 + }, 641 + type: 'object', 642 + }; 643 + 644 + if (isRequired) { 645 + irDiscriminatorSchema.required = [discriminator.propertyName]; 646 + } 647 + schemaItems.push(irDiscriminatorSchema); 549 648 } 550 649 } 551 650