Skip to content

Commit 2ffa4c7

Browse files
committed
Add old behavior
Signed-off-by: xil <fridalu66@gmail.com>
1 parent adf4bc5 commit 2ffa4c7

5 files changed

Lines changed: 129 additions & 108 deletions

File tree

DEVELOPER_GUIDE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -234,8 +234,8 @@ npm config set registry https://registry.npmjs.org/
234234
3. **Generate Protobuf**
235235

236236
```bash
237-
export OPENAPI_GENERATOR_VERSION=7.19.0
238-
npx @openapitools/openapi-generator-cli generate \
237+
export OPENAPI_GENERATOR_VERSION=7.19.0
238+
npx @openapitools/openapi-generator-cli generate \
239239
-c tools/proto-convert/src/config/protobuf-generator-config.yaml
240240
```
241241

tools/proto-convert/src/SchemaModifier.ts

Lines changed: 85 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import logger from "./utils/logger";
77

88
const DEFAULT_MAP_KEY = 'field' // default key for simplified additionalProperties
99
const DEFAULT_MAP_VALUE = 'value' // default value for simplified additionalProperties
10+
const QUERY_CONTAINER_SCHEMA_NAME = 'QueryContainer' // schema name that requires special inline handling.
1011

1112
export class SchemaModifier {
1213
root: OpenAPIV3.Document;
@@ -37,8 +38,8 @@ export class SchemaModifier {
3738
});
3839
const visit = new Set();
3940
traverse(this.root, {
40-
onSchemaProperty: (schema) => {
41-
this.simplifySingleMapSchema(schema, visit);
41+
onSchemaProperty: (schema, propertyName, parentSchemaName) => {
42+
this.simplifySingleMapSchema(schema, visit, parentSchemaName);
4243
this.handleAdditionalPropertiesUndefined(schema)
4344

4445
},
@@ -279,7 +280,6 @@ export class SchemaModifier {
279280

280281
/**
281282
* Extracts type name from $ref or title
282-
* Note: Schema names have already been sanitized by Sanitizer, so '___' prefixes are already removed
283283
**/
284284
private getTypeName(schema: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject): string | null {
285285
if ('$ref' in schema) {
@@ -293,58 +293,115 @@ export class SchemaModifier {
293293
}
294294

295295
/**
296-
* Transforms SchemaObject that single-key maps (`minProperties = 1` and `maxProperties = 1`) into a new wrapper schema.
297-
*
296+
* Transforms SchemaObject that single-key maps (`minProperties = 1` and `maxProperties = 1`) into a $ref to a new map schema.
298297
*
299-
* Example:
300-
* Input:
298+
* Input (items of DecayFunction):
301299
* {
302300
* type: "object",
301+
* propertyNames: { title: "field", type: "string" },
303302
* additionalProperties: {
304-
* $ref: "#/components/schemas/SortOrder"
303+
* title: "placement",
304+
* $ref: "#/components/schemas/DecayPlacement"
305305
* },
306306
* minProperties: 1,
307307
* maxProperties: 1,
308308
* }
309309
*
310310
* Output:
311+
* - Replaces with: { $ref: "#/components/schemas/DecayPlacementSingleMap" }
312+
* - Creates new schema DecayPlacementSingleMap:
311313
* {
312314
* type: "object",
313-
* title: "SortOrderMap",
314315
* properties: {
315316
* field: { type: "string" },
316-
* sort_order: { $ref: "#/components/schemas/SortOrder" }
317+
* decay_placement: {
318+
* title: "placement",
319+
* $ref: "#/components/schemas/DecayPlacement"
320+
* }
317321
* },
318-
* required: ["field", "sort_order"]
322+
* required: ["field", "decay_placement"]
319323
* }
320324
*
321325
**/
322-
simplifySingleMapSchema(schema: OpenAPIV3.SchemaObject, visit: Set<any>): void {
326+
simplifySingleMapSchema(schema: OpenAPIV3.SchemaObject, visit: Set<any>, parentSchemaName?: string): void {
323327
if (schema.type === 'object' && typeof schema.additionalProperties === 'object' &&
324328
!Array.isArray(schema.additionalProperties) && schema.minProperties === 1 && schema.maxProperties === 1){
325329

330+
// Check if this is a QueryContainer property
331+
// If so, use the old inline behavior to avoid breaking QueryContainer structure
332+
// TODO: Remove this special case in next major release and use SingleMap wrapper for all schemas
333+
if (parentSchemaName === QUERY_CONTAINER_SCHEMA_NAME) {
334+
logger.info(`Using inline modification for ${QUERY_CONTAINER_SCHEMA_NAME} property (legacy behavior)`+JSON.stringify(schema));
335+
// Old behavior: inline modification
336+
const reconstructAdditionalPropertySchema = this.reconstructAdditionalPropertySchema(schema.additionalProperties, visit);
337+
Object.assign(schema, reconstructAdditionalPropertySchema);
338+
339+
delete schema.additionalProperties;
340+
delete schema.minProperties;
341+
delete schema.maxProperties;
342+
delete schema.type;
343+
if ('propertyNames' in schema) {
344+
delete schema.propertyNames;
345+
}
346+
return;
347+
}
348+
349+
// New behavior: create intermediate SingleMap schema
326350
const valueSchema = schema.additionalProperties;
327-
const typeName = this.getTypeName(valueSchema) || 'Value'; // Use 'Value' as fallback
328-
329-
// Create new wrapper schema
330-
// Avoid collision with the reserved 'field' key property name
331-
const rawPropertyName = toSnakeCase(typeName);
332-
const valuePropertyName = rawPropertyName === 'field' ? `${rawPropertyName}_value` : rawPropertyName;
333-
const wrapperTitle = schema.title || `${typeName}Map`;
334-
335-
schema.title = wrapperTitle;
336-
schema.properties = {
337-
field: { type: 'string' as const },
338-
[valuePropertyName]: valueSchema
351+
const typeName = this.getTypeName(valueSchema) || 'Value';
352+
353+
// Extract field property name and schema from propertyNames
354+
let fieldPropertyName = 'field';
355+
let fieldPropertySchema: any = { type: 'string' as const };
356+
357+
if ((schema as any).propertyNames) {
358+
const propertyNames = (schema as any).propertyNames;
359+
360+
// If propertyNames has a title, use it as the property name
361+
if (propertyNames.title && typeof propertyNames.title === 'string') {
362+
fieldPropertyName = propertyNames.title;
363+
364+
fieldPropertySchema = { ...propertyNames };
365+
delete fieldPropertySchema.title;
366+
} else {
367+
// Use propertyNames as-is for the field schema
368+
fieldPropertySchema = propertyNames;
369+
}
370+
}
371+
372+
// Create new map schema name
373+
const mapSchemaName = `${typeName}SingleMap`;
374+
375+
// Create the new map schema
376+
const newMapSchema: OpenAPIV3.SchemaObject = {
377+
type: 'object',
378+
properties: {
379+
[fieldPropertyName]: fieldPropertySchema,
380+
[toSnakeCase(typeName)]: valueSchema
381+
},
382+
required: [fieldPropertyName, toSnakeCase(typeName)]
339383
};
340-
schema.required = ['field', valuePropertyName];
341384

385+
// Add to components.schemas if not already exists
386+
if (!this.root.components) {
387+
this.root.components = {};
388+
}
389+
if (!this.root.components.schemas) {
390+
this.root.components.schemas = {};
391+
}
392+
if (!this.root.components.schemas[mapSchemaName]) {
393+
this.root.components.schemas[mapSchemaName] = newMapSchema;
394+
}
395+
396+
// Replace current schema with $ref to the new map schema
397+
// Only delete the properties we don't need, keep vendor extensions (x-*)
398+
delete schema.type;
342399
delete schema.additionalProperties;
343400
delete schema.minProperties;
344401
delete schema.maxProperties;
345-
if ('propertyNames' in schema) {
346-
delete schema.propertyNames;
347-
}
402+
delete (schema as any).propertyNames;
403+
404+
(schema as any).$ref = `#/components/schemas/${mapSchemaName}`;
348405
}
349406
}
350407

tools/proto-convert/src/config/spec-filter.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ excluded_schemas:
1818
- CombinedFieldsQuery
1919
- DistanceFeatureQuery
2020
- GeoPolygonQuery
21+
- GeoShapeQuery
2122
- HasChildQuery
2223
- HasParentQuery
2324
- MoreLikeThisQuery

tools/proto-convert/src/utils/OpenApiTraverser.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ export type SchemaVisitorSet = {
55
onResponseSchema?: (schema: OpenAPIV3.SchemaObject, name: string) => void;
66
onRequestSchema?: (schema: OpenAPIV3.SchemaObject, name: string) => void;
77
onParameter?: (param: OpenAPIV3.ParameterObject, name: string) => void;
8-
onSchemaProperty?: (schema: OpenAPIV3.SchemaObject, propertyName: string) => void;
8+
onSchemaProperty?: (schema: OpenAPIV3.SchemaObject, propertyName: string, parentSchemaName?: string) => void;
99
};
1010

1111
/**
@@ -24,7 +24,7 @@ export function traverse(
2424
const schema = components.schemas[schemaName];
2525
visitors.onSchema?.(schema, schemaName);
2626
if (!('$ref' in schema)) {
27-
traverseSchema(schema, visitors);
27+
traverseSchema(schema, visitors, schemaName);
2828
}
2929
}
3030
}
@@ -79,7 +79,8 @@ export function traverse(
7979
*/
8080
export function traverseSchema(
8181
schema: OpenAPIV3.SchemaObject,
82-
visitors: SchemaVisitorSet
82+
visitors: SchemaVisitorSet,
83+
parentSchemaName?: string
8384
): void {
8485
if (!schema) return;
8586

@@ -88,21 +89,23 @@ export function traverseSchema(
8889
for (const propName in schema.properties) {
8990
const propSchema = schema.properties[propName];
9091
if (!('$ref' in propSchema)) {
91-
visitors.onSchemaProperty?.(propSchema, propName);
92-
traverseSchema(propSchema, visitors); // recurse into nested properties
92+
visitors.onSchemaProperty?.(propSchema, propName, parentSchemaName);
93+
traverseSchema(propSchema, visitors, parentSchemaName); // recurse into nested properties
9394
}
9495
}
9596
}
9697

9798
// Visit items (for array types)
9899
if (schema.type === 'array' && schema.items && typeof schema.items === 'object' && !('$ref' in schema.items)) {
99-
traverseSchema(schema.items as OpenAPIV3.SchemaObject, visitors);
100+
const itemsSchema = schema.items as OpenAPIV3.SchemaObject;
101+
visitors.onSchemaProperty?.(itemsSchema, 'items', parentSchemaName);
102+
traverseSchema(itemsSchema, visitors, parentSchemaName);
100103
}
101104

102105
// Visit additionalProperties
103106
if (schema.additionalProperties && typeof schema.additionalProperties === 'object' && !('$ref' in schema.additionalProperties)
104107
) {
105-
traverseSchema(schema.additionalProperties as OpenAPIV3.SchemaObject, visitors);
108+
traverseSchema(schema.additionalProperties as OpenAPIV3.SchemaObject, visitors, parentSchemaName);
106109
}
107110

108111
// Visit composed schemas
@@ -113,7 +116,7 @@ export function traverseSchema(
113116
for (const sub of subschemas) {
114117
if (!('$ref' in sub)) {
115118
visitors.onSchema?.(sub, `${key}`);
116-
traverseSchema(sub, visitors);
119+
traverseSchema(sub, visitors, parentSchemaName);
117120
}
118121
}
119122
}

tools/proto-convert/test/SchemaModifier.test.ts

Lines changed: 30 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -623,9 +623,8 @@ describe('SchemaModifier', () => {
623623
});
624624

625625
describe('simplifySingleMapSchema', () => {
626-
it('should create wrapper schema with sanitized names (real-world usage)', () => {
626+
it('should create new map schema and reference it', () => {
627627
const doc = createDocument();
628-
// After Sanitizer runs, schema names have '___' prefixes removed
629628
doc.components!.schemas!.SortOrder = {
630629
type: 'string',
631630
enum: ['asc', 'desc']
@@ -645,69 +644,23 @@ describe('SchemaModifier', () => {
645644

646645
modifier.simplifySingleMapSchema(schema, visit);
647646

648-
expect(schema.type).toBe('object');
649-
expect(schema.title).toBe('SortOrderMap');
650-
expect(schema.properties).toBeDefined();
651-
expect(schema.properties.field).toEqual({ type: 'string' });
652-
expect(schema.properties.sort_order).toEqual({
647+
// Schema should be replaced with $ref
648+
expect(schema.$ref).toBe('#/components/schemas/SortOrderSingleMap');
649+
expect(schema.type).toBeUndefined();
650+
expect(schema.properties).toBeUndefined();
651+
652+
// New SortOrderSingleMap schema should be created
653+
expect(doc.components!.schemas!.SortOrderSingleMap).toBeDefined();
654+
const mapSchema = doc.components!.schemas!.SortOrderSingleMap as OpenAPIV3.SchemaObject;
655+
expect(mapSchema.type).toBe('object');
656+
expect(mapSchema.properties!.field).toEqual({ type: 'string' });
657+
expect(mapSchema.properties!.sort_order).toEqual({
653658
$ref: '#/components/schemas/SortOrder'
654659
});
655-
expect(schema.required).toEqual(['field', 'sort_order']);
656-
expect(schema.additionalProperties).toBeUndefined();
657-
expect(schema.minProperties).toBeUndefined();
658-
expect(schema.maxProperties).toBeUndefined();
659-
});
660-
661-
it('should handle schema with title', () => {
662-
const doc = createDocument();
663-
const modifier = new SchemaModifier(doc);
664-
const visit = new Set();
665-
666-
const schema: any = {
667-
type: 'object',
668-
title: 'CustomTitle',
669-
additionalProperties: {
670-
type: 'string',
671-
title: 'Value'
672-
},
673-
minProperties: 1,
674-
maxProperties: 1
675-
};
676-
677-
modifier.simplifySingleMapSchema(schema, visit);
678-
679-
expect(schema.title).toBe('CustomTitle');
680-
expect(schema.properties.field).toBeDefined();
681-
expect(schema.properties.value).toEqual({ type: 'string', title: 'Value' });
682-
});
683-
684-
it('should convert PascalCase to snake_case for property name', () => {
685-
const doc = createDocument();
686-
doc.components!.schemas!.MyComplexType = {
687-
type: 'object',
688-
properties: {
689-
prop1: { type: 'string' }
690-
}
691-
};
692-
693-
const modifier = new SchemaModifier(doc);
694-
const visit = new Set();
695-
696-
const schema: any = {
697-
type: 'object',
698-
additionalProperties: {
699-
$ref: '#/components/schemas/MyComplexType'
700-
},
701-
minProperties: 1,
702-
maxProperties: 1
703-
};
704-
705-
modifier.simplifySingleMapSchema(schema, visit);
706-
707-
expect(schema.properties.my_complex_type).toBeDefined();
660+
expect(mapSchema.required).toEqual(['field', 'sort_order']);
708661
});
709662

710-
it('should remove propertyNames if present', () => {
663+
it('should use propertyNames ref for field property', () => {
711664
const doc = createDocument();
712665
doc.components!.schemas!.Field = {
713666
type: 'string'
@@ -733,32 +686,39 @@ describe('SchemaModifier', () => {
733686

734687
modifier.simplifySingleMapSchema(schema, visit);
735688

736-
expect(schema.propertyNames).toBeUndefined();
737-
expect(schema.properties.field).toBeDefined();
738-
expect(schema.properties.sort_order).toBeDefined();
689+
// Check new map schema uses propertyNames for field
690+
const mapSchema = doc.components!.schemas!.SortOrderSingleMap as OpenAPIV3.SchemaObject;
691+
expect(mapSchema.properties!.field).toEqual({
692+
$ref: '#/components/schemas/Field'
693+
});
739694
});
740695

741-
it('should rename value property to field_value when type name collides with field', () => {
696+
it('should convert PascalCase to snake_case for property name', () => {
742697
const doc = createDocument();
743-
doc.components!.schemas!.Field = { type: 'string' };
698+
doc.components!.schemas!.MyComplexType = {
699+
type: 'object',
700+
properties: {
701+
prop1: { type: 'string' }
702+
}
703+
};
744704

745705
const modifier = new SchemaModifier(doc);
746706
const visit = new Set();
747707

748708
const schema: any = {
749709
type: 'object',
750710
additionalProperties: {
751-
$ref: '#/components/schemas/Field'
711+
$ref: '#/components/schemas/MyComplexType'
752712
},
753713
minProperties: 1,
754714
maxProperties: 1
755715
};
756716

757717
modifier.simplifySingleMapSchema(schema, visit);
758718

759-
expect(schema.properties.field).toEqual({ type: 'string' });
760-
expect(schema.properties.field_value).toEqual({ $ref: '#/components/schemas/Field' });
761-
expect(schema.required).toEqual(['field', 'field_value']);
719+
expect(doc.components!.schemas!.MyComplexTypeSingleMap).toBeDefined();
720+
const mapSchema = doc.components!.schemas!.MyComplexTypeSingleMap as OpenAPIV3.SchemaObject;
721+
expect(mapSchema.properties!.my_complex_type).toBeDefined();
762722
});
763723

764724
it('should not modify when not single-key map', () => {

0 commit comments

Comments
 (0)