Skip to content
Open
9 changes: 6 additions & 3 deletions packages/codegen-core/src/symbols/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,15 @@ export class SymbolRegistry implements ISymbolRegistry {
}
}

private buildIndexKeySpace(meta: ISymbolMeta, prefix = ''): IndexKeySpace {
const entries: Array<IndexEntry> = [];
private buildIndexKeySpace(
meta: ISymbolMeta,
prefix = '',
entries: Array<IndexEntry> = [],
): IndexKeySpace {
for (const [key, value] of Object.entries(meta)) {
const path = prefix ? `${prefix}.${key}` : key;
if (value && typeof value === 'object' && !Array.isArray(value)) {
entries.push(...this.buildIndexKeySpace(value as ISymbolMeta, path));
this.buildIndexKeySpace(value as ISymbolMeta, path, entries);
} else {
entries.push([path, value, `${path}:${JSON.stringify(value)}`]);
}
Expand Down
26 changes: 13 additions & 13 deletions packages/shared/src/ir/graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,15 @@ export const irTopLevelKinds = [

export type IrTopLevelKind = (typeof irTopLevelKinds)[number];

const irPatterns: Record<IrTopLevelKind, RegExp> = {
operation: /^#\/paths\/[^/]+\/(get|put|post|delete|options|head|patch|trace)$/,
parameter: /^#\/components\/parameters\/[^/]+$/,
requestBody: /^#\/components\/requestBodies\/[^/]+$/,
schema: /^#\/components\/schemas\/[^/]+$/,
server: /^#\/servers\/(\d+|[^/]+)$/,
webhook: /^#\/webhooks\/[^/]+\/(get|put|post|delete|options|head|patch|trace)$/,
};

/**
* Checks if a pointer matches a known top-level IR component (schema, parameter, etc) and returns match info.
*
Expand All @@ -19,21 +28,12 @@ export type IrTopLevelKind = (typeof irTopLevelKinds)[number];
* @returns { matched: true, kind: IrTopLevelKind } | { matched: false } - Whether it matched, and the matched kind if so
*/
export const matchIrPointerToGroup: MatchPointerToGroupFn<IrTopLevelKind> = (pointer, kind) => {
const patterns: Record<IrTopLevelKind, RegExp> = {
operation: /^#\/paths\/[^/]+\/(get|put|post|delete|options|head|patch|trace)$/,
parameter: /^#\/components\/parameters\/[^/]+$/,
requestBody: /^#\/components\/requestBodies\/[^/]+$/,
schema: /^#\/components\/schemas\/[^/]+$/,
server: /^#\/servers\/(\d+|[^/]+)$/,
webhook: /^#\/webhooks\/[^/]+\/(get|put|post|delete|options|head|patch|trace)$/,
};
if (kind) {
return patterns[kind].test(pointer) ? { kind, matched: true } : { matched: false };
return irPatterns[kind].test(pointer) ? { kind, matched: true } : { matched: false };
}
for (const key of Object.keys(patterns)) {
const kind = key as IrTopLevelKind;
if (patterns[kind].test(pointer)) {
return { kind, matched: true };
for (const key of irTopLevelKinds) {
if (irPatterns[key].test(pointer)) {
return { kind: key, matched: true };
}
}
return { matched: false };
Expand Down
19 changes: 9 additions & 10 deletions packages/shared/src/ir/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,23 +64,22 @@ export function deduplicateSchema<T extends IR.SchemaObject>({
uniqueItems.push(item);
}

let result = { ...schema };
result.items = uniqueItems;
const result = { ...schema };

if (
result.items.length <= 1 &&
uniqueItems.length <= 1 &&
result.type !== 'array' &&
result.type !== 'enum' &&
result.type !== 'tuple'
) {
// bring the only item up to clean up the schema
const liftedSchema = result.items[0];
delete result.logicalOperator;
delete result.items;
result = {
...result,
...liftedSchema,
};
const liftedSchema = uniqueItems[0];
result.items = undefined;
result.logicalOperator = undefined;

Object.assign(result, liftedSchema);
} else {
result.items = uniqueItems;
}

// exclude unknown if it's the only type left
Expand Down
5 changes: 1 addition & 4 deletions packages/shared/src/ir/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,7 @@ export function addItemsToSchema({

if (mutateSchemaOneItem) {
// bring composition up to avoid extraneous brackets
schema = {
...schema,
...items[0],
};
Object.assign(schema, items[0]);
return schema;
}

Expand Down
2 changes: 1 addition & 1 deletion packages/shared/src/openApi/2.0.x/parser/pagination.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ export const paginationField = ({
const property = schema.properties[name]!;

if (typeof property !== 'boolean' && !('$ref' in property)) {
const schemaType = getSchemaType({ schema: property });
const schemaType = getSchemaType(property);
// TODO: resolve deeper references

if (isPaginationType(schemaType)) {
Expand Down
72 changes: 37 additions & 35 deletions packages/shared/src/openApi/2.0.x/parser/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,9 @@ import type {
import { discriminatorValues } from '../../../openApi/shared/utils/discriminator';
import { isTopLevelComponent, refToName } from '../../../utils/ref';

export function getSchemaType({
schema,
}: {
schema: OpenAPIV2.SchemaObject;
}): SchemaType<OpenAPIV2.SchemaObject> | undefined {
export function getSchemaType(
schema: OpenAPIV2.SchemaObject,
): SchemaType<OpenAPIV2.SchemaObject> | undefined {
if (schema.type) {
return schema.type;
}
Expand Down Expand Up @@ -139,10 +137,7 @@ function parseArray({
const ofArray = schema.items.allOf;
if (ofArray && ofArray.length > 1 && !schema.items['x-nullable']) {
// bring composition up to avoid incorrectly nested arrays
irSchema = {
...irSchema,
...irItemsSchema,
};
Object.assign(irSchema, irItemsSchema);
} else {
schemaItems.push(irItemsSchema);
}
Expand Down Expand Up @@ -198,24 +193,30 @@ function parseObject({
}): IR.SchemaObject {
irSchema.type = 'object';

const schemaProperties: Record<string, IR.SchemaObject> = {};
let isSchemaPropertiesEmpty = true;

for (const name in schema.properties) {
const property = schema.properties[name]!;
if (typeof property === 'boolean') {
// TODO: parser - handle boolean properties
} else {
const irPropertySchema = schemaToIrSchema({
context,
schema: property,
state,
});
schemaProperties[name] = irPropertySchema;
if (schema.properties) {
const schemaProperties: Record<string, IR.SchemaObject> = {};

for (const name in schema.properties) {
isSchemaPropertiesEmpty = false;

const property = schema.properties[name]!;
if (typeof property === 'boolean') {
// TODO: parser - handle boolean properties
} else {
const irPropertySchema = schemaToIrSchema({
context,
schema: property,
state,
});
schemaProperties[name] = irPropertySchema;
}
}
}

if (Object.keys(schemaProperties).length) {
irSchema.properties = schemaProperties;
if (!isSchemaPropertiesEmpty) {
irSchema.properties = schemaProperties;
}
}

if (schema.additionalProperties === undefined) {
Expand All @@ -230,7 +231,7 @@ function parseObject({
const isEmptyObjectInAllOf =
state.inAllOf &&
schema.additionalProperties === false &&
(!schema.properties || !Object.keys(schema.properties).length);
(!schema.properties || isSchemaPropertiesEmpty);

if (!isEmptyObjectInAllOf) {
irSchema.additionalProperties = {
Expand Down Expand Up @@ -302,7 +303,7 @@ function parseAllOf({
let irSchema = initIrSchema({ schema });

const schemaItems: Array<IR.SchemaObject> = [];
const schemaType = getSchemaType({ schema });
const schemaType = getSchemaType(schema);

const compositionSchemas = schema.allOf;

Expand All @@ -318,13 +319,10 @@ function parseAllOf({
state,
});
state.inAllOf = originalInAllOf;
if (state.inAllOf === undefined) {
delete state.inAllOf;
}

if (schema.required) {
if (irCompositionSchema.required) {
irCompositionSchema.required = [...irCompositionSchema.required, ...schema.required];
irCompositionSchema.required.push(...schema.required);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same aliasing concern: 2.0.x's parseType also does irSchema.required = schema.required without copying (see line 251 of this file), so irCompositionSchema.required can alias the source spec. This push will mutate the input document. .concat(...) would preserve the perf intent while keeping the array fresh.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically, this is OK, we don't use original required anywhere else but to turn them into irSchema.required. Also, the unit test says passed, so I expected this to be fine.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same context as the 3.1.x thread — the residual concern is a single $ref'd component used from multiple allOf parents, where the first push pollutes the spec node read by the second. Not blocking; deferring to you since tests pass.

Pullfrog  | View workflow run | via Pullfrog | Using Claude Opus𝕏

} else {
irCompositionSchema.required = schema.required;
}
Expand Down Expand Up @@ -380,7 +378,7 @@ function parseAllOf({
? context.resolveRef<OpenAPIV2.SchemaObject>(compositionSchema.$ref)
: compositionSchema;

if (getSchemaType({ schema: finalCompositionSchema }) === 'object') {
if (getSchemaType(finalCompositionSchema) === 'object') {
const irCompositionSchema = parseOneType({
context,
schema: {
Expand Down Expand Up @@ -459,8 +457,12 @@ function parseEnum({
irSchema.type = 'enum';

const schemaItems: Array<IR.SchemaObject> = [];
const xEnumDescriptions = schema['x-enum-descriptions'];
const xEnumVarnames = schema['x-enum-varnames'];
const xEnumNames = schema['x-enumNames'];

for (const [index, enumValue] of schema.enum.entries()) {
for (let index = 0, len = schema.enum.length; index < len; index++) {
const enumValue = schema.enum[index];
const typeOfEnumValue = typeof enumValue;
let enumType: SchemaType<OpenAPIV2.SchemaObject> | 'null' | undefined;

Expand Down Expand Up @@ -492,8 +494,8 @@ function parseEnum({
const irTypeSchema = parseOneType({
context,
schema: {
description: schema['x-enum-descriptions']?.[index],
title: schema['x-enum-varnames']?.[index] ?? schema['x-enumNames']?.[index],
description: xEnumDescriptions?.[index],
title: xEnumVarnames?.[index] ?? xEnumNames?.[index],
// cast enum to string temporarily
type: enumType === 'null' ? 'string' : enumType,
},
Expand Down Expand Up @@ -630,7 +632,7 @@ function parseType({

parseSchemaMeta({ irSchema, schema });

const type = getSchemaType({ schema });
const type = getSchemaType(schema);

if (!type) {
return irSchema;
Expand Down
2 changes: 1 addition & 1 deletion packages/shared/src/openApi/3.0.x/parser/pagination.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export const paginationField = ({
const property = schema.properties[name]!;

if (typeof property !== 'boolean' && !('$ref' in property)) {
const schemaType = getSchemaType({ schema: property });
const schemaType = getSchemaType(property);
// TODO: resolve deeper references

if (isPaginationType(schemaType)) {
Expand Down
Loading
Loading